#! /bin/sh
#
# Demo, curl-based shell script to sync current directory to a store-it
# instance.
#
# It uses:
#
# - a bourne shell (tested on GNU bash, version 3.2.33)
# - curl (tested on curl 7.18.2)
# - find (tested on GNU findutils 4.4.0)
# - grep (tested on GNU grep 2.5.1)
# - a seder (tested on GNU sed 4.1.5)
# - sha1sum
#
# Sylvain Fourmanoit, 2009.
#
#------------------------------------------------------------------------------
# Utils
#
log() {
    echo "`date` - $*" 1>&2
}

die() {
    log "(FATAL) $*"
    exit 1
}

warn() {
    log "(WARNING) $*"
    false
}

#------------------------------------------------------------------------------
# Communication functions
#
get_auth() {
    ${CURL} -fsS -c ${JAR} ${BASE}/${AUTH} || \
	warn 'failed getting authenticated'
}

get_list() {
    log 'getting server file list...'
    ${CURL} -f -b ${JAR} ${BASE}/list -o ${LIST} || \
	warn 'failed getting server list'
}

put_file() {
    log "uploading ${F}"
    ${CURL} -f -b ${JAR} -T ".${F}" ${BASE}/${F}
}

delete_file() {
    log "deleting ${F}"
    ${CURL} -f -b ${JAR} -X DELETE ${BASE}/${F}
}

retry() {
    RETRIES=$1 ; shift
    for I in `seq ${RETRIES}`; do
	eval "$*" && break
	log "retry ${I}/${RETRIES}"
	sleep 1
	false
    done
    
}

cleanup() {
    for F in ${JAR} ${LIST} ; do
	test -e ${F} && rm ${F}
    done
}

usage() {
    cat<<EOF
Usage: store-it.sh [OPTION] local_directory
       store-id.sh -d|--delete remote_file_or_directory

Sync local_directory (current working directory if '.' used)
to a remote store-it site, or delete a remote file or directory.

OPTIONS are:
  -h,--help	  Display this help message.
  -n,--dry-run    Print what would be changed remotely, 
                  but do not perform the changes.
  -k,--keep       Update all files, but do not remove
                  remote files that have been suppressed
                  locally.
  -m,--mtime DAYS Only take into account local files that had 
                  been modified in the last DAYS days. This
                  is a serious speed-up when you deal with big
                  hierarchies.

See $HOME/.store-it for settings.
EOF
}

#------------------------------------------------------------------------------
# Import preferences
if test -e ${HOME}/.store-it ; then
    source ${HOME}/.store-it
else
    (
    cat<<EOF
# store-it.sh script variables

# Defines the base store-it URL
#BASE=http://myapp.appspot.org/

# Defines where to get cookie from server
#AUTH=my_auth_cookie_path

# Alternative curl executable
#CURL=/usr/bin/curl

# Directory for temporaty files
#TMPDIR=/tmp

EOF
    ) > ${HOME}/.store-it
    log "settings stored in ${HOME}/.store-it"
fi

# Set a few defaults, just in case
: ${BASE:=http://myapp.appspot.com/}
: ${AUTH:=myauth_cookie_path}
: ${CURL:=`which curl`}
: ${TMPDIR:=/tmp}

# Install cleanup hook
trap 'cleanup' INT TERM EXIT

# Setup and secure temporary files
JAR=${TMPDIR}/jar$$
LIST=${TMPDIR}/list$$

for F in ${JAR} ${LIST} ; do
    touch ${F}
    chmod 600 ${F}
done

#------------------------------------------------------------------------------
# Parse command line
DRY_RUN=0 ; KEEP=0; DIR= ; OPWD=`pwd`; FIND_PARAMS= ; DELETE=0
while test $# -gt 0 ; do
    case "$1" in
	-h|--help)
	    usage
	    exit 0
	    ;;
	-n|--dry-run)
	    DRY_RUN=1
	    ;;
	-k|--keep)
	    KEEP=1
	    ;;
	-m|--mtime)
	    FIND_PARAMS="-mtime -$2"
	    shift
	    ;;
	-d|--delete)
	    DELETE=1;
	    ;;
	*)
	    test "x`echo $1 | sed '/^-/!d'`" = x || {
		usage
		die "unknown flag '$1'"
	    }
	    DIR=$1
	    ;;
    esac
    shift
done

test "x${DIR}" = x && {
    usage
    die 'local_directory or remote_file_or_directory not given'
}


#------------------------------------------------------------------------------
# Main routine

# Delete mode
test ${DELETE} -eq 1 && {
    test ${DRY_RUN} -eq 1 && \
	die 'no dry run mode for deletion'

    get_auth
    F=${DIR}
    retry 3 delete_file || \
	die "could not delete remote file or directory ${F}"
    exit 0
}

# Sync mode
(
    cd ${DIR} || \
	die "could not change to local directory '${DIR}'"

    # Get auth, then get list
    get_auth
    retry 3 get_list || \
	die 'could not get the list'

    # Compute the list of what have changed locally
    UP=
    for LOCAL in `find . -type f ${FIND_PARAMS}`; do 
	REMOTE=`echo ${LOCAL} | sed 's/^.//'`
	LSHA1=`sha1sum ${LOCAL} | sed 's/ .*$//'`
	
	RSHA1=`grep "[[:space:]]${REMOTE}$" ${LIST}` 
	if test $? -eq 0 ; then
	    if echo ${RSHA1} | grep -q ${LSHA1} ; then
		log "${REMOTE} unchanged (no op)"
	    else
		log "${REMOTE} changed (reupload)"
		UP="${UP} ${REMOTE}"
	    fi
	else
	    log "${REMOTE} is new (upload)"
	    UP="${UP} ${REMOTE}"
	fi
    done

    # Compute the list of what has been removed
    DOWN=
    for REMOTE in `sed '1d;s/.*\t//' ${LIST}` ; do
	test -e ".${REMOTE}" || {
	    log "${REMOTE} locally removed (delete)"
	    DOWN="${DOWN} ${REMOTE}"
	}
    done

    # Act on the lists:
    if test ${DRY_RUN} -eq 1 ; then
	cat<<EOF

Upload
======
${UP}

Delete (if allowed)
===================
${DOWN}
EOF
    else
	for F in ${UP} ; do
	    get_auth
	    retry 3 put_file || \
		die "could not upload file ${F}"
	done
	test ${KEEP} -eq 1 || {
		for F in ${DOWN} ; do
		    get_auth
		    retry 3 delete_file || \
			die "could not delete file ${F}"
		done
	    }
    fi
)

#------------------------------------------------------------------------------
# That's all, folks!
